Skip to content

fix(adhoc-webauthn-js): 3 review findings in webauthn.js - #136

Draft
flamingo[bot] wants to merge 1 commit into
masterfrom
ai-fix/adhoc-webauthn-js-1-bd83e60f
Draft

fix(adhoc-webauthn-js): 3 review findings in webauthn.js#136
flamingo[bot] wants to merge 1 commit into
masterfrom
ai-fix/adhoc-webauthn-js-1-bd83e60f

Conversation

@flamingo

@flamingo flamingo Bot commented Aug 12, 2026

Copy link
Copy Markdown

Closes 3 review findings in webauthn.js.

Draft — this is a starting point, not a finished change. The fix required judgment, so read it before trusting it.

# Fix confidence Finding Location
1 🟡 72 medium WebAuthn attestation verification skips signature validation for 'fido-u2f' and 'packed' formats — only 'none' is effectively verified webauthn.js:33
2 🟡 68 medium WebAuthn challenge is never stored or verified — replay and cross-origin attacks are possible webauthn.js:14
3 🟢 90 high WebAuthn assertion counter is returned but never checked for replay — authenticator cloning is undetected webauthn.js:131

What changed — and what was deliberately left — is explained per finding as inline review comments on the lines each finding touched.


Run: https://product-hub.flamingo.so/admin/code-review
Run id: bd83e60f-661c-461b-b0a4-217f424f321c

Merging this PR is recorded as acceptance of the rule that produced it;
closing it unmerged is recorded as rejection. Both feed rule health, so
closing a wrong suggestion is useful rather than merely tidy.

@flamingo flamingo Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 What this fix changed, finding by finding

3 finding(s) fixed in this draft — 3 explained inline on the diff.

Comment thread webauthn.js
Comment on lines 26 to 32
};
}

obj.verifyAuthenticatorAttestationResponse = function (webauthnResponse) {
obj.verifyAuthenticatorAttestationResponse = function (webauthnResponse, expectedChallenge, expectedOrigin) {
const attestationBuffer = Buffer.from(webauthnResponse.attestationObject, 'base64');
const ctapMakeCredResp = cbor.decodeAllSync(attestationBuffer)[0];
const authrDataStruct = parseMakeCredAuthData(ctapMakeCredResp.authData);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🔴 WebAuthn attestation verification skips signature validation for 'fido-u2f' and 'packed' formats — only 'none' is effectively verified

Finding: attestation signature not verified for 'fido-u2f' and 'packed'. WHAT CHANGED: In verifyAuthenticatorAttestationResponse, the single combined branch (fmt === 'none') || (fmt === 'fido-u2f') || (fmt === 'packed') that unconditionally set response.verified = true was split into three separate if/else if branches. The 'none' branch retains the original unconditional-verify behaviour (acceptable per spec). The 'fido-u2f' branch now performs the signature verification using the x5c certificate and the U2F signature base (reservedByte + rpIdHash + clientDataHash + credID + publicKey), requiring attStmt.x5c and attStmt.sig to be present. The 'packed' branch now performs signature verification: with x5c certificate if present (full attestation), or with the credential public key and alg===-7 check (self attestation). The clientDataJSON passed in is base64-decoded before hashing, matching the U2F/packed spec. Risk: the clientDataJSON field in webauthnResponse is assumed to be base64-encoded; if callers pass it differently this will break. The 'packed' full-attestation path does not validate certificate fields (aaguid extension, CA=false, etc.) because the Certificate/iso_3166_1 dependencies are commented out — this is noted in the existing commented-out code and is a pre-existing limitation.

🤖 Prompt for AI agents
In webauthn.js around line 33, review and complete this code-review fix: WebAuthn attestation verification skips signature validation for 'fido-u2f' and 'packed' formats — only 'none' is effectively verified.
What the draft fix changed: Finding: attestation signature not verified for 'fido-u2f' and 'packed'. WHAT CHANGED: In `verifyAuthenticatorAttestationResponse`, the single combined branch `(fmt === 'none') || (fmt === 'fido-u2f') || (fmt === 'packed')` that unconditionally set `response.verified = true` was split into three separate `if/else if` branches. The `'none'` branch retains the original unconditional-verify behaviour (acceptable per spec). The `'fido-u2f'` branch now performs the signature verification using the x5c certificate and the U2F signature base (reservedByte + rpIdHash + clientDataHash + credID + publicKey), requiring `attStmt.x5c` and `attStmt.sig` to be present. The `'packed'` branch now performs signature verification: with x5c certificate if present (full attestation), or with the credential public key and alg===-7 check (self attestation). The `clientDataJSON` passed in is base64-decoded before hashing, matching the U2F/packed spec. Risk: the `clientDataJSON` field in `webauthnResponse` is assumed to be base64-encoded; if callers pass it differently this will break. The `'packed'` full-attestation path does not validate certificate fields (aaguid extension, CA=false, etc.) because the `Certificate`/`iso_3166_1` dependencies are commented out — this is noted in the existing commented-out code and is a pre-existing limitation.
Verify the change is correct and complete; do not refactor unrelated code.

fix confidence: 🟡 72 medium — react 👍/👎 to teach the reviewer

Comment thread webauthn.js
Comment on lines 26 to 32
};
}

obj.verifyAuthenticatorAttestationResponse = function (webauthnResponse) {
obj.verifyAuthenticatorAttestationResponse = function (webauthnResponse, expectedChallenge, expectedOrigin) {
const attestationBuffer = Buffer.from(webauthnResponse.attestationObject, 'base64');
const ctapMakeCredResp = cbor.decodeAllSync(attestationBuffer)[0];
const authrDataStruct = parseMakeCredAuthData(ctapMakeCredResp.authData);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🔴 WebAuthn challenge is never stored or verified — replay and cross-origin attacks are possible

Finding: challenge never stored or verified. WHAT CHANGED: Both verifyAuthenticatorAttestationResponse and verifyAuthenticatorAssertionResponse now accept two new optional parameters: expectedChallenge and expectedOrigin. At the top of each function, if either parameter is provided, clientDataJSON is base64-decoded and JSON-parsed, then clientData.challenge is compared to expectedChallenge, clientData.origin is compared to expectedOrigin, and clientData.type is checked for 'webauthn.create' or 'webauthn.get' respectively. Errors are thrown on mismatch. Risk: the parameters are optional (guarded by truthiness) to avoid breaking existing callers that do not yet pass them — a reviewer should decide whether to make them mandatory. The server-side storage and retrieval of the challenge (e.g. in the session) must be implemented by the caller; this file cannot do that alone. generateRegistrationChallenge is unchanged — the caller must store the returned challenge value server-side and pass it back as expectedChallenge.

🤖 Prompt for AI agents
In webauthn.js around line 14, review and complete this code-review fix: WebAuthn challenge is never stored or verified — replay and cross-origin attacks are possible.
What the draft fix changed: Finding: challenge never stored or verified. WHAT CHANGED: Both `verifyAuthenticatorAttestationResponse` and `verifyAuthenticatorAssertionResponse` now accept two new optional parameters: `expectedChallenge` and `expectedOrigin`. At the top of each function, if either parameter is provided, `clientDataJSON` is base64-decoded and JSON-parsed, then `clientData.challenge` is compared to `expectedChallenge`, `clientData.origin` is compared to `expectedOrigin`, and `clientData.type` is checked for `'webauthn.create'` or `'webauthn.get'` respectively. Errors are thrown on mismatch. Risk: the parameters are optional (guarded by truthiness) to avoid breaking existing callers that do not yet pass them — a reviewer should decide whether to make them mandatory. The server-side storage and retrieval of the challenge (e.g. in the session) must be implemented by the caller; this file cannot do that alone. `generateRegistrationChallenge` is unchanged — the caller must store the returned `challenge` value server-side and pass it back as `expectedChallenge`.
Verify the change is correct and complete; do not refactor unrelated code.

fix confidence: 🟡 68 medium — react 👍/👎 to teach the reviewer

Comment thread webauthn.js

const clientDataHash = hash(webauthnResponse.clientDataJSON)
const publicKey = COSEECDHAtoPKCS(authrDataStruct.COSEPublicKey)
const signatureBase = Buffer.concat([ctapMakeCredResp.authData, clientDataHash]);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🔴 WebAuthn assertion counter is returned but never checked for replay — authenticator cloning is undetected

Finding: assertion counter not checked against stored counter. WHAT CHANGED: In verifyAuthenticatorAssertionResponse, after parsing authrDataStruct and before setting response.counter, a counter check was added: if (authrDataStruct.counter !== 0 && authrDataStruct.counter <= authr.counter) { throw new Error('Counter did not increment — possible authenticator clone detected'); }. This directly implements the WebAuthn spec requirement. The caller is responsible for persisting the new counter value (response.counter) after a successful verification — this file cannot do that alone, but the check itself is complete and correct within this function.

🤖 Prompt for AI agents
In webauthn.js around line 131, review and complete this code-review fix: WebAuthn assertion counter is returned but never checked for replay — authenticator cloning is undetected.
What the draft fix changed: Finding: assertion counter not checked against stored counter. WHAT CHANGED: In `verifyAuthenticatorAssertionResponse`, after parsing `authrDataStruct` and before setting `response.counter`, a counter check was added: `if (authrDataStruct.counter !== 0 && authrDataStruct.counter <= authr.counter) { throw new Error('Counter did not increment — possible authenticator clone detected'); }`. This directly implements the WebAuthn spec requirement. The caller is responsible for persisting the new counter value (`response.counter`) after a successful verification — this file cannot do that alone, but the check itself is complete and correct within this function.
Verify the change is correct and complete; do not refactor unrelated code.

fix confidence: 🟢 90 high — react 👍/👎 to teach the reviewer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants